/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.chain;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import tal.drivechain.chain.script.ScriptGenerator;
import tal.drivechain.module.xmlconfig.PropertyInfo;
import tal.drivechain.parsing.ParsingAid;
import tal.drivechain.plugin.Channel;
import tal.drivechain.plugin.Logger;
import tal.drivechain.system.Globals;
import tal.drivechain.variable.SetProperty.Flag;
import tal.drivechain.variable.VarTable;
import tal.drivechain.variable.VarTableListener;
import tal.drivechain.variable.Variable;

/**
 *
 * @author eisental
 */
public abstract class ChainNode implements ScriptGenerator, VarTableListener {
    private static final List<String> reservedKeywords = Arrays.asList(new String[] {"chain", "this", "parent", "module", "root", "node"});

    public enum MSG {INFO, ANOUNCE, WARNING, ERROR, DEBUG};

    protected List<Logger> loggers = new ArrayList<Logger>();
    protected boolean infoFlag, debugFlag;
    protected boolean canHaveOutputs = true;
    protected NodeInfo nodeInfo;

    private List<ChainNode> inputs = new ArrayList<ChainNode>();
    private List<ChainNode> outputs = new ArrayList<ChainNode>();
    private Chain parent;
    protected String nodeName;
    private boolean isRunning = false;
    private HashMap<String,String> metaData = new HashMap<String,String>();

    protected VarTable localVars = new VarTable();
    protected List<NodeListener> nodeListeners = new ArrayList<NodeListener>();

    public void setParentChain(Chain parent) {
        boolean isMoved = (this.parent!=null);

        if (isMoved && parent==this.parent) return;
        if (isMoved)
            this.parent.getVarTable().removeListener(this);

        
        this.parent = parent;
        if (parent!=null)
            this.parent.getVarTable().addListener(this);
        if (isMoved) for (NodeListener l : nodeListeners) l.nodeMoved(this);
    }
    
    public Chain getParentChain() { return this.parent; }

    public List<Logger> getLoggers() {
//        System.out.println("Retrieving loggers from " + this.getPath());
//        for (Logger l : loggers)
//            System.out.println(" * " + l.getClass().getName());
        return loggers;
    }

    public abstract String getNodeClassName();

    /**
     * Sends a message to the {@link Logger} of this module.
     *
     *
     * @param message       Text message that will be used by the logger
     * @param type          The type of the message. It can be Module.MSG.INFO, Module.MSG.ANOUNCE,
     *                      Module.MSG.WARNING, Module.MSG.ERROR, Module.MSG.DEBUG.
     */
    public void log(String message, MSG type) {
        if (type==MSG.DEBUG && !debugFlag) return;
        if (type==MSG.INFO && !infoFlag) return;
        for (Logger l : loggers) {
            l.nodeMessage(message, type, this);
        }
    }

    public abstract Object getProperty(String name);

    public abstract void setProperty(String propName, Object newValue, Flag flag) ;

    public NodeInfo getNodeInfo() { return nodeInfo; }

    public abstract Object getOriginalPropValue(String name);

    public static boolean isNodeNameLegal(String name) {
        if (!ParsingAid.SYMBOLNAME_PATTERN.matcher(name).matches()) return false;
        else if (reservedKeywords.contains(name)) return false;
        else return true;
    }

    public abstract PropertyInfo getPropertyInfo(String prop);

    public void removeLogger(Logger l) {
        loggers.remove(l);
    }

    public void addLogger(Logger l) {
        if (!loggers.contains(l)) {
            loggers.add(l);
        }
    }

    public boolean getInfoFlag() {
        return infoFlag;
    }

    public void setInfoFlag(boolean infoFlag) {
        this.infoFlag = infoFlag;
    }

    public boolean getDebugFlag() {
        return debugFlag;
    }

    public void setDebugFlag(boolean debugFlag) {
        this.debugFlag = debugFlag;
    }

    public void setName(String name) {
        boolean nameChanged = (this.nodeName!=null);
        if (name!=null && !isNodeNameLegal(name))
            throw new IllegalArgumentException("Illegal node name: '" + name + "'");

        this.nodeName = name;
        if (nameChanged) {
            for (NodeListener l : nodeListeners.toArray(new NodeListener[0])) l.nameChanged(this);
            if (this.getParentChain()!=null) this.getParentChain().dispatchChildRenamed(this);
        }
    }

    public String getName() { return this.nodeName; }

    public VarTable getVarTable() { return localVars; }

    public String getPath() {
        Chain curParent = this.getParentChain();
        String ret = this.getName();
        while (curParent!=null) {
            ret = curParent.getName() + "." + ret;
            curParent = curParent.getParentChain();
        }
        return ret;
    }

    public boolean canHaveOutputs() { return canHaveOutputs; }

    public void start() {
        isRunning = true;
    };

    public void stop() {
        isRunning = false;
    };

    public boolean isRunning() { return isRunning; }

    public void send(Variable in, long timestamp, Channel channel) {
        for (NodeListener l : nodeListeners) l.messageReceived(this, in, channel, timestamp);
    }

    public void addOutput(ChainNode out) {
        if (canHaveOutputs) {
            if (outputs.contains(out))
                throw new IllegalArgumentException("Node " + out.getName() + " is already connected to the output of " + this.nodeName);
            out.addInput(this);
            outputs.add(out);
        } else throw new IllegalArgumentException("Node " + this.getName() + " does not have an output jack.");
    }

    public void addInput(ChainNode input) {
        inputs.add(input);
        for (NodeListener l : nodeListeners)
            l.connectionsChanged(this);
    }

    public void addNodeListener(NodeListener l) { nodeListeners.add(l); }
    public void removeNodeListener(NodeListener l) { nodeListeners.remove(l); }

    public boolean removeOutput(ChainNode a) {
        boolean ret = outputs.remove(a);
        for (NodeListener l : nodeListeners)
            l.connectionsChanged(this);
        return ret;
    }

    public boolean removeInput(ChainNode a) {
        boolean ret = inputs.remove(a);
        for (NodeListener l : nodeListeners)
            l.connectionsChanged(this);
        return ret;
    }

    public void disconnect() {
        for (ChainNode o : outputs)
            o.removeInput(this);
        for (ChainNode i : inputs)
            i.removeOutput(this);
        outputs.clear();
        inputs.clear();
        for (NodeListener l : nodeListeners)
            l.connectionsChanged(this);

    }

    public List<ChainNode> getInputs() { return Collections.unmodifiableList(inputs); }

    /**
     * Returns the module that's connected to this modules's output.
     */
    public List<ChainNode> getOutputs() { return Collections.unmodifiableList(outputs); }

    public void destroy() {
        stop();
        disconnect();

        if (parent!=null) parent.removeChild(this.getName());
        else if (this==Globals.getRootChain()) Globals.setRootChain(Chain.createRootChain());

        List<NodeListener> oldListeners = new ArrayList<NodeListener>();
        for (NodeListener l : nodeListeners) oldListeners.add(l);
        for (NodeListener l : oldListeners) l.nodeDestroyed(this);
    }

    public void reset() {
        stop();
        localVars = new VarTable();
        metaData.clear();
    }

    public void putMetadata(String key, String value) {
        metaData.put(key, value);
        for (NodeListener l : nodeListeners) {
            l.metaChanged(this);
        } 
    }

    public String removeMetadata(String key) {
        String meta = metaData.remove(key);
        for (NodeListener l : nodeListeners) l.metaChanged(this);
        return meta;
    }

    public String getMetadata(String key) {
        return metaData.get(key);
    }

    public Map<String,String> getMetadataMap() {
        return Collections.unmodifiableMap(metaData);
    }


    protected String generateMetaDataScript() {
        String script = "";
        for (String key : this.metaData.keySet())
            if (!key.startsWith(".")) script += "** " + key + ": " + metaData.get(key) + ";\n";
        return script;
    }

    protected String generateHiddenMetaDataScript() {
        String script = "";
        for (String key : this.metaData.keySet())
            if (key.startsWith(".")) script += "** " + key + ": " + metaData.get(key) + ";\n";
        return script;
    }


    public abstract void setupProperties() throws IllegalArgumentException, AbortChainException;

    public abstract boolean isPropertyGettable(String propName);
    public abstract boolean isPropertySettable(String propName);

    /**
     * Sends a {@link Variable} object and a timestamp to the module connected to this module's output.
     * In case the Variable is set to not be evaluated localy (the ~ sign is used before the data type in it's declaration)
     * it will be evaluated before sending it further using this Modules localVars field.
     *
     * @param out       The {@link Variable} that will be sent to the modules output.
     * @param timestamp A timestamp value that is sent together with the Variable.
     */
    protected void sendToOutputs(Variable out, long timestamp) { // mass send of a message
        if (isRunning()) {
            Variable send;
            if (out.isEvaluateAtHome()) {
                try {
                    send = out.evaluate(this);
                } catch (IllegalArgumentException ie) {
                    log(ie.getMessage(), MSG.WARNING);
                    return;
                }

            } else send = out;

            if (send==null) { log("Trying to send null variable.", MSG.DEBUG); return; }
            if (debugFlag) log("Sending: " + send.toString(this) + " (" + timestamp + ")", MSG.DEBUG);
            for (ChainNode n : this.getOutputs()) {
                if (debugFlag) log("to: " + n.getName(), MSG.DEBUG);
                n.send(send, timestamp, Channel.CHAIN);
            }
            for (NodeListener l : nodeListeners) l.messageSent(this, out, timestamp);
        }
    }

    public abstract Variable findVariable(String name);

    public void setVarEvent(Variable message) {
        if (this.isRunning())
            send(message, -1, Channel.SETVAR);
    }

    public void globalMessageEvent(Variable message) {
        if (this.isRunning()) {
            send(message, -1, Channel.GLOBALMESSAGE);
        }
    }

    public void removeVarEvent(Variable var) {
    }

    public void varScopeChanged(Variable var, boolean isPublic) { 
    }
}
